package Controller;

/**
 *<h2>Parse magstripe <b>Track 1</b> &amp; <b>Track 2</b> data.</h2>
 *
Object for parsing magstripe track 1 and track 2 data strings.
<br /><br />
Example Track 1 & 2 card data string
<pre><code>
%B1234123412341234^CardUser/John^030510100000019301000000877000000?;1234123412341234=0305101193010877?
</code></pre>
This class <i>should</i> be able to recognize and standardize any of the following input data strings:
<ol>
<li>Track 1 & Track 2 strings combined (w/sentinels)</li>
<li>Track 1 string only w/out sentinels</li>
<li>Track 2 data only w/out sentinels</li>

</ol>

<br /><br />
You can set a number of properties for controlling the classes behavior.
<ul>
<li><b>com.acmetech.cc.magstripe.debug</b> if true, writes trace info to System.out.
<li><b>com.acmetech.cc.magstripe.autoGenerateTrack2</b> if true (default) will
generate track2 data using track 1 data.
</ul>
@author <a href="http://www.acmetech.com/">Wayne K. Walrath, Acme Technologies</a>

<br /><br />

<font size="-1">
<u>Version History</u>
<br />
<font size="-1"><b>0.1 - 2005-03-23</b></font> - First release
</font>

 * <br /><br />
 * <font size="-1">Copyright 2005 &copy; Acme Technologies. All rights reserved. Permission granted to
 * incorporate these classes into non-commercial (shareware applications ARE allowed)
 * derivative works. You may not post these classes on the internet or otherwise distribute
 * them EXCEPT as part of a derivative work which includes the Acme Technologies copyright
 * and the address of our website (www.acmetech.com).
 * <br /><br />For an easy flat-fee redistribution license for commercial use, please contact
 * Acme Technologies through one of the means listed 
 * at <a href="http://www.acmetech.com/">www.acmetech.com</a>.
 * </font>
 */

public class MagStripeCard
{
	private static final String PROP_DEBUG = "com.acmetech.cc.magstripe.debug";
	private static final String PROP_AUTO_TRACK2 = "com.acmetech.cc.magstripe.autoGenerateTrack2";
	
	/**
	 * raw input string containing track 1, track 2 or track 1 &amp; 2 data.
	 */
	String _InputStripeStr = null;
	
	/**
	 * track 1 data string (parsed from input) including sentinel characters.
	 */
	String _Track1Data = null;

	/**
	 * track 2 data string (parsed from input) including sentinel characters.
	 * @see #_AutoGenerateTrack2
	 */
	String _Track2Data = null;

	/**
	 * if set to true, parsing and debugging info written to System.out
	 */
	static protected boolean _Debug = false;
	
	/**
	 * set to true after input magnetic stripe string has been parsed.
	 */
	boolean _NeedsParsing = true;

	/**
	 * true if track 1 data present
	 */
	boolean _HasTrack1 = false;
	
	/**
	 * true if track 2 data present (note: track 2 is autmatically generated
	 * if _AutoGenerateTrack2 is set to true -- default's to true)
	 */
	boolean _HasTrack2 = false;
	
	/**
	 * If set to true (default) track2 data is automatically generated
	 * if it was not supplied.
	 */
	static public boolean _AutoGenerateTrack2 = true;

	/**
	 * True if <b>Track 2</b> magnetic stripe data was auto-generated from the Track
	 * 1 data.
	 */
	boolean _Track2WasAutoGenerated = false;
	
	/**
	 * Raw account name (something like &quot;Creditmeister/Steven P&quot;)
	 */
	String _AccountHolder = null;
	
	/**
	 * Account holder first name (plus possibly middle initial, title, etc)
	 */
	String _NameFirst = null;
	
	/**
	 * Account holder surname
	 */
	String _NameLast = null;
	
	/**
	 * Primary Account Number
	 */
	String _PAN = null;
	
	/**
	 * Expiration month (two digit)
	 */
	String _ExpMonthStr = null;
	/**
	 * Expiration year (four digit)
	 */
	String _ExpYearStr = null;
	
	/**
	 * Create a MagStripeCard using magnetic stripe data string
	 * (tracks 1, 2 or both)
	 * @param trackString track 1,2, or both data strings.
	 * @throws MagstripeParseException
	 */
	public MagStripeCard(String trackString)
		throws MagstripeParseException{
		this._InputStripeStr = trackString;
		this._NeedsParsing = true;
		String p = System.getProperty(PROP_DEBUG, "false");
		if( p.equalsIgnoreCase("true") )
			_Debug = true;
		p = System.getProperty(PROP_AUTO_TRACK2, "true");
		if( ! p.equalsIgnoreCase("true") )
			_AutoGenerateTrack2 = false;
		_parse();
	}

	/**
	 * Determine if track 1 data is present.
	 * @return boolean indicating whether track 1 data is present
	 */
	public boolean hasTrack1() {
		return _HasTrack1;
	}

	/**
	 * Determine if track 2 data is present.
	 * @return boolean indicating whether track 2 data is present
	 */
	public boolean hasTrack2(){
		return _HasTrack2;
	}

	/**
	 * Get Track 1 magstripe string.
	 * @return the Track 1 magnetic stripe data string or null if no Track 1 data exists.
	 * @throws MagstripeParseException
	 * @see #getTrack2()
	 */
	public String getTrack1(){
		return _Track1Data;
	}
	/**
	 * Get Track 2 magstripe string.
	 * @return the Track 2 magnetic stripe data string or null if no Track 1 data exists
	 * @see #getTrack1()
	 */
	public String getTrack2(){
		return _Track2Data;
	}

	/**
	 * Get the combined Track 1 &amp; 2 data string if both are available.
	 * @return the magnetic track data string if both are available (possibly Track 2 was
	 * auto-generated), or null if both are not available.
	 * @see #getTrack1()
	 * @see #getTrack2()
	 */
	public String getTrackData(){
		if( _HasTrack1 && _HasTrack2 )
			return _Track1Data + _Track2Data;
		return null;
	}

	/**
	 * Determine whether the <b>Track 2</b> data string was auto-generated during parsing.
	 * @return true if the Track 2 data was auto-generated.
	 */
	public boolean getTrack2WasAutoGenerated(){
		return _Track2WasAutoGenerated;
	}

	/**
	 * Get the raw account holder name (ex.: &quot;Spears/Brintney S&quot;).
	 * @return The raw account holder name as parsed from Track 1 data or null if no Track 1 data.
	 */
	public String getAccountName(){
		return _AccountHolder;
	}
	/**
	 * Get account holder first name
	 * @return account holder first name or null if no Track 1 data.
	 */
	public String getFirstName(){
		return _NameFirst;
	}
	/**
	 * Get account holder last name
	 * @return account holder last name or null if no Track 1 data.
	 */
	public String getLastName(){
		return _NameLast;
	}
	/**
	 * Get primary account number (PAN field from Track 1 or Track 2 data)
	 * @return PAN or null if account not found.
	 */
	public String getAccountNumber(){
		return _PAN;
	}
	/**
	 * Get expiration month from track 1 or track 2 data.
	 * @return two-digit expiration month string.
	 */
	public String getExpirationMonth(){
		return _ExpMonthStr;
	}
	/**
	 * Get expiration year from track 1 or track 2 data.
	 * @return four-digit expiration year string.
	 */
	public String getExpirationYear(){
		return _ExpYearStr;
	}
	
	/**
	 * parse the magstripe string. Called automatically as needed.
	 * @throws MagstripeParseException
	 */
	protected void _parse() throws MagstripeParseException {
		if( ! this._NeedsParsing )
			return;
		try{
			w("==== Parsing Input String ====");
			//-- Example: Track 1 Data Only
			//-- %B1234123412341234^CardUser/John^030510100000019301000000877000000?
			//-- Key off of the presence of "^" but not "="
			
			//-- Example: Track 2 Data Only
			//-- ;1234123412341234=0305101193010877?
			//-- Key off of the presence of "=" but not "^"
			//--- Determine the presence of special characters
			int iHasTrack1 = _InputStripeStr.indexOf("^");
			int iHasTrack2 = _InputStripeStr.indexOf("=");
			
			if( iHasTrack1 > 0 )
				_HasTrack1 = true;
			if( iHasTrack2 > 0 )
				_HasTrack2 = true;

			if( _HasTrack1 && ! _HasTrack2 ){
				//-- set track 1 data
				if( _InputStripeStr.charAt(0) != '%' )
					_InputStripeStr = "%" + _InputStripeStr;
				if( _InputStripeStr.charAt(_InputStripeStr.length()-1) != '?' )
					_InputStripeStr += "?";
				_Track1Data = _InputStripeStr;
			}

			//-- regenerate these indices
			iHasTrack1 = _InputStripeStr.indexOf("^");
			iHasTrack2 = _InputStripeStr.indexOf("=");
			
			if( _HasTrack1 && _HasTrack2 ){
				//-- Split t1 and t2 strings into member fields
				int t2start = _InputStripeStr.indexOf(";");
				if( t2start == -1 )
					throw new MagstripeParseException("Cannot locate start of track2 data (;)");
					
				_Track1Data = _InputStripeStr.substring(0,t2start);
				w("Track 1=" + _Track1Data);
				_Track2Data = _InputStripeStr.substring(t2start);
				w("Track 2=" + _Track2Data);

				//--
				//-- At this point we should parse t2 acct/exp. date and
				//-- verify it matches t1 values (TO DO)
				//--
			}
			
			String sAcctName = null;
			String sAcct = null;
			String sExpDate = null;
			if( _HasTrack1 ){
				int iFS2 = _InputStripeStr.lastIndexOf("^");
				if( iFS2 == -1 )
					throw new MagstripeParseException("Missing (2nd) field separator (^) in track 1 data");
				sAcct = _InputStripeStr.substring(0,iHasTrack1);
				// look for starting sentinel and format code
				if( sAcct.charAt(0) == '%' ){
					if( ! Character.isDigit(sAcct.charAt(1) ) ){
						sAcct = sAcct.substring(2);
					}else{
						sAcct = sAcct.substring(1);
					}
				}
				_PAN = sAcct;
				w("Track 1 Account: " + sAcct);

				sAcctName = _InputStripeStr.substring(iHasTrack1+1, iFS2);
				_AccountHolder = sAcctName;
				w("Track 1 Acct Name: " + sAcctName);
				int iNameDelim = sAcctName.indexOf("/");
				if( iNameDelim == -1 )
					throw new MagstripeParseException("Missing delimiter [/] in account holder name data");
				_NameFirst = sAcctName.substring(0, iNameDelim);
				_NameLast = sAcctName.substring(iNameDelim+1);
				w("First Name: " + _NameFirst);
				w("Last Name: " + _NameLast);

				//-- date format: YYMM
				sExpDate = _InputStripeStr.substring(iFS2+1,iFS2+5);
				w("Track 1 Acct Exp Date: " + sExpDate);
				_ExpYearStr = "20" + sExpDate.substring(0,2);
				w("Exp Year: " + _ExpYearStr);
				_ExpMonthStr =  sExpDate.substring(2,4);
				w("Exp Month: " + _ExpMonthStr);
				

				if( ! _HasTrack2 && _AutoGenerateTrack2 ){
					// create pseudo track 2 data
			        _Track2Data = ";" + sAcct + "=" + sExpDate + "111111111111?";
					_HasTrack2 = _Track2WasAutoGenerated = true; //set flag now
					w("Auto-generated track 2: " + _Track2Data);
				}
			}

			if( _HasTrack2 ){
				// may have already parsed this info out if track 1 data present
				if( _PAN == null ){
					//------------------------------------------
					//--- Track 2 only cards
					//--- Ex: 1234123412341234=0305101193010877?
					//------------------------------------------
					_Track2Data = _InputStripeStr;
					//-- add sentinels if not there
					if( _Track2Data.charAt(0) != ';' )
						_Track2Data = ";" + _Track2Data;
					if( _Track2Data.charAt(_Track2Data.length()-1) != '?' )
						_Track2Data += "?";

					w("Track 2 Data=" + _Track2Data);
					
					int iSep = _Track2Data.indexOf("=");
					if( iSep == -1 )
						throw new MagstripeParseException("Invalid track 2 data string");
					_PAN = _Track2Data.substring(0,iSep);
					if( _PAN.charAt(0) == ';' )
						_PAN = _PAN.substring(1);
					w("Track 2 Card Num: " + _PAN);
					
					_ExpYearStr = "20" + _Track2Data.substring(iSep+1,iSep+3);
					w("Track 2 Exp. Year: " + _ExpYearStr);
					_ExpMonthStr = _Track2Data.substring(iSep+3,iSep+5) ;
					w("Track 2 Exp Month: " + _ExpMonthStr);
				}
			}
		}catch (ArrayIndexOutOfBoundsException oob){
			throw new MagstripeParseException(oob.getMessage());
		}catch(NumberFormatException nfe){
			throw new MagstripeParseException(nfe.getMessage());
		}catch (Exception exc){
			throw new MagstripeParseException(exc.getMessage());		
		}
		this._NeedsParsing = false;	
	}
	
	
	
	/**
	 * Static test method called with a single parameter (the track 1, 2 or combined mag data string)
	 * Call with no parameters for sample usage string.
	 * @param args a string containing magnetic track 1, 2 or combined data.
	 * @return nothing. writes to stdout.
<p>
Sample cmd line and output:<br />
<font size="-1"><code><pre>
java -classpath . -Dcom.acmetech.cc.magstripe.debug=true \ 
	-Dcom.acmetech.cc.magstripe.autoGenerateTrack2=false 	\
	com.acmetech.cc.MagStripeCard 	\
	'%B1234123412341234^CardUser/John^030510100000019301000000877000000?;1234123412341234=0305101193010877?'

==> ==== Parsing Input String ====
==> Track 1=%B1234123412341234^CardUser/John^030510100000019301000000877000000?
==> Track 2=;1234123412341234=0305101193010877?
==> Track 1 Account: 1234123412341234
==> Track 1 Acct Name: CardUser/John
==> First Name: CardUser
==> Last Name: John
==> Track 1 Acct Exp Date: 0305
==> Exp Year: 2003
==> Exp Month: 05
==> 
==> ===== Testing Public Methods =====
==> Has Track 1?	true
==> Has Track 2?	true
==> Track 1 String=%B1234123412341234^CardUser/John^030510100000019301000000877000000?
==> Track 2 String=;1234123412341234=0305101193010877?
==> Track 2 AUTO-GENERATED: false
==> Combined Tracks=%B1234123412341234^CardUser/John^030510100000019301000000877000000?;1234123412341234=0305101193010877?
==> Account Name (RAW): CardUser/John
==> First Name: CardUser
==> Last Name: John
==> Primary Account Num (PAN): 1234123412341234
==> Expiration Month: 05
==> Expiration Year: 2003
==> -- DONE --
</pre></code></font></p>
	 
	 */
	public static void main( String args[] ){
		
		if( args.length == 0 ){
			System.err.println("Usage: java com.acmetech.cc.MagStripeCard <magstripe_data>");
			System.err.println("Usage: java -classpath . -Dcom.acmetech.cc.magstripe.debug=true -Dcom.acmetech.cc.autoGenerateTrack2=false com.acmetech.cc.MagStripeCard '%B1234123412341234^CardUser/John^030510100000019301000000877000000?;1234123412341234=0305101193010877?'");
			System.exit(1);
		}
		try{
		MagStripeCard ms = new MagStripeCard(args[0]);
		w("");
		w("===== Testing Public Methods =====");
		w("Has Track 1?\t" + ms.hasTrack1() );
		w("Has Track 2?\t" + ms.hasTrack2() );
		w("Track 1 String=" + ms.getTrack1() );
		w("Track 2 String=" + ms.getTrack2() );
		w("Track 2 AUTO-GENERATED: " + ms.getTrack2WasAutoGenerated());
		w("Combined Tracks=" + ms.getTrackData());
		w("Account Name (RAW): " + ms.getAccountName() );
		w("First Name: " + ms.getFirstName() );
		w("Last Name: " + ms.getLastName() );
		w("Primary Account Num (PAN): " + ms.getAccountNumber());
		w("Expiration Month: " + ms.getExpirationMonth());
		w("Expiration Year: " + ms.getExpirationYear());
		w("-- DONE --");
		}catch (MagstripeParseException parseEx){
			parseEx.printStackTrace();
		}
	}
	
	/**
	 * output a string to System.out
	 * @param msg String to write to System.out
	 *
	 */
	private static void w(String msg){
		if( _Debug )
			System.out.println("==> " + msg);
	}
}
